(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
本篇內容以react-router v5為主,react-router在v6後有大幅度改變,可參考官方文件或是下方邦友回覆
https://reactrouter.com/en/main/upgrading/v5
在過去,當我們要製作「分頁」時,多半是新增一個靜態HTML檔,讓web server根據檔案路徑去尋找,或是透過後端程式碼去定義什麼url要對應到哪個HTML檔。這種方式我們稱為伺服器渲染(SSR)
然而這卻也產生了一個問題。
即使頁面中大多是固定的Layout,但換頁的時候,因為是拜訪新檔案,整個頁面都要刷新。
為了解決這個問題,工程師決定也用Javascript從去創造前端路由控制器。換頁的時候,只用JS去改變不一樣的地方。這樣的網頁程式換頁時不需要整頁都刷新,使用起來跟APP很像,因此又稱為Single Page Application(SPA,單頁式網頁應用)。也因為大多統一成一個JS檔並改在瀏覽器製造頁面,這樣的方式也稱為客戶端渲染(CSR)。
React-router-dom就是在React達成前端路由的插件之一。他是基於React-router這個核心製作,衍伸的家族還有在react-native使用的react-router-native。
這裡我們的Menu.js
、MenuItem.js
會依照Day.12的程式,InputForm.js
會依照Day.15的程式。然後我們要新增src/page資料夾,並在裡面製作兩個用來當作分頁的頁面:
import React from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
let menuItemWording=[
"Like的發問",
"Like的回答",
"Like的文章",
"Like的留言"
];
const MenuPage = () =>{
let menuItemArr = menuItemWording.map((wording) => <MenuItem text={wording}/>);
return <Menu title={"Andy Chang的like"}>{menuItemArr}</Menu>;
}
export default MenuPage;
import React from 'react';
import InputForm from '../component/InputForm';
const FormPage = () =>{
return <InputForm/>;
}
export default FormPage;
接下來,我們會嘗試在src/index.js來控制並創造控制分頁的路由。
請打開terminal,並輸入
npm i react-router-dom --save
安裝完畢後,進入src/index.js,在開頭引入
import React from 'react';
import ReactDOM from 'react-dom';
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";
import {HashRouter,Route,Switch,Link} from "react-router-dom";
其中這一行會是所有我們要用到的元件
import {HashRouter,Route,Switch} from "react-router-dom";
路由器的英文是Router,但為甚麼這裡要加一個Hash呢? 這是因為如果我們要從前端去判斷當前的url是什麼,必須要在根路徑最後方加入一個#
。JS才能從#
後方的字串去判斷。
當然,React-router-dom也有提供不會有#
的BrowserRouter
。但這個會需要後端的配合,我們目前只有純前端檔案就先用HashRouter。
現在,請在src/index.js創造一個元件App
,讓React程式統一從這個元件渲染。並在裡面先加入<HashRouter></HashRouter>
。
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter,Route,Switch} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";
const App = () =>{
return(
<HashRouter>
</HashRouter>
);
}
ReactDOM.render(
<App/>,
document.getElementById('root')
);
Switch這個元件是用來正確地判斷路由應該對應到誰。我們一樣在src/index.js裡面加入<Switch></Switch>
。
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter,Route,Switch} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";
const App = () =>{
return(
<HashRouter>
<Switch>
</Switch>
</HashRouter>
);
}
ReactDOM.render(
<App/>,
document.getElementById('root')
);
Route就是用來設定分頁的元件,用path
這個props來設定url字串,它的使用方法有兩種:
React.creactElement
<Route path="/form" component={FormPage}/>
<Route path="/form" render={()=>{return( <FormPage/> )}}/>
平常會用第一種方式,但如果你想要在分頁元件上綁props,就要用第二種。
現在,把我們的分頁元件用Route加進App中:
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter,Route,Switch} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";
const App = () =>{
return(
<HashRouter>
<Switch>
<Route exact={true} path="/" component={MenuPage}/>
<Route path="/form" component={FormPage}/>
</Switch>
</HashRouter>
);
}
ReactDOM.render(
<App/>,
document.getElementById('root')
);
為什麼這裡<Route exact={true} path="/" component={MenuPage}/>
要加上exact={true}
呢? 這是因為path="/form"
當中也有包含/
。React router dom在檢查路由時是依照順序的,如果今天用戶拜訪了path="/form"
,React router dom會在檢查<Route path="/" component={MenuPage}/>
時就認定包含/
路由,所以顯示MenuPage
。
exact
這個props就是用來限定路由一定要完全跟path一模一樣才顯示。因為是布林值,你也可以只寫名字不給值。
這樣就完成了分頁。
然而這樣並沒有顯示出SPA的感覺。所以現在我們來做一個固定的導覽列。請在src/index.js上方新增一個Layout元件:
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter,Route,Switch} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";
const Layout = () => {
return(
)
}
const App = () =>{
/* 省略 */
然後在App中使用Layout把所有Route包起來:
const App = () =>{
return(
<HashRouter>
<Switch>
<Layout>
<Route exact path="/" component={MenuPage}/>
<Route path="/form" component={FormPage}/>
</Layout>
</Switch>
</HashRouter>
);
}
然後我們就能在Layout中以props.children來顯示對應的Route
const Layout = (props) => {
return(
<>
{ props.children }
</>
)
}
但是目前我們還缺少了前往分頁的導覽列,請在Layout中新增<nav>
const Layout = (props) => {
return(
<>
<nav>
</nav>
{ props.children }
</>
)
}
接下來就是要加入超連結了。
一般講到超連結,我們會聯想到<a href="/路徑">
,但這裡我們要使用的是React-router-dom提供的原件<Link>
。
為什麼要特別多弄一個元件呢?
這是因為<a href="/路徑">
是預設導向主domain/路徑
,當我們今天使用的是subdomain或是像hash router這種東西時,就要自己把subdomain或是#
補進去,像是<a href="/#/路徑">
。這樣當我們今天專案部屬環境不同時就很麻煩。
Link這個元件就會方便我們導向/統一管理要導向的路徑。它的語法是
<Link to="路徑">
現在,我們在開頭引入Link這個元素,並使用在<nav></nav>
中。
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter,Route,Switch,Link} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";
const Layout = (props) => {
return(
<>
<nav>
<Link to="/">點我連到第一頁</Link>
<Link to="/form" style={{marginLeft:"20px"}}>點我連到第二頁</Link>
</nav>
{ props.children }
</>
)
}
所有的程式碼:
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter,Route,Switch,Link} from "react-router-dom";
import MenuPage from "./page/MenuPage";
import FormPage from "./page/FormPage";
const Layout = (props) => {
return(
<>
<nav>
<Link to="/">點我連到第一頁</Link>
<Link to="/form" style={{marginLeft:"20px"}}>點我連到第二頁</Link>
</nav>
{ props.children }
</>
)
}
const App = () =>{
return(
<HashRouter>
<Switch>
<Layout>
<Route exact path="/" component={MenuPage}/>
<Route path="/form" component={FormPage}/>
</Layout>
</Switch>
</HashRouter>
);
}
ReactDOM.render(
<App/>,
document.getElementById('root')
);
執行結果:
Link還能透過location api傳資料。詳請請參考官方文件
CSR延伸的問題是因為程式碼都用JS處理,導致SEO的時候只會抓到原本那個空的<div id="root"></div>
。為了解決這個問題,衍伸出了在後端製作React網頁(SSR)的方法。我們最後面會回頭來講這個
Hi 最近這一兩周都在看你的文章,謝謝您的文章,作為入門的我來說真的很詳細很好懂!
我也有買你的書來看,不過目前有兩個回饋想跟您說一下:
一、Switch由Routes取代,且Route中component關鍵字改成element
<Routes>
<Route path='/' element={<h1>Home Page Component</h1>} />
<Route path='/login' element={<h1>Login Page Component</h1>} />
// New line
<Route path='*' element={<Navigate to='/' />} />
</Routes>
二、不得在Routes/BrowserRoute中包含自訂Component或Layout
以前可以這樣:
<BrowserRouter>
<Switch>
<Layout>
<Route exact path="/" component={FirstPage}/>
</Layout>
</Switch>
</BrowserRouter>
現在要改成這樣:
<BrowserRouter>
<Routes>
<Route exact path="/" element={
<Layout>
<FirstPage />
</Layout>
}/>
</Routes>
</BrowserRouter>
三、this.props.match在DOM v6由useLocation、useParams取代
DOM v6之前:
const NotePage = ({match}) => {
let noteId = match.params.id
}
DOM v6之後:
取得參數部分,由useParams取代,取得現在的URL,則變成是要用useLocation():
const NotePage = () => {
let {id} = useParams();
const location = useLocation();
console.log(location.pathname);
}
四、Redirect Component在DOM v6中被Navigate Component取代
DOM v6之前:
<Switch>
<Route exact path={`${this.props.match.path}`} component={Introd} />
<Route path={`${this.props.match.path}/his`} component={His} />
//...
<Redirect from={`${this.props.match.path}/story`} to={`${this.props.match.url}/his`} />
</Switch>
DOM v6之後:
<Routes>
<Route path='/' element={<h1>Home Page Component</h1>} />
<Route path='/login' element={<h1>Login Page Component</h1>} />
// New line
<Route path='*' element={<Navigate to='/' />} />
</Routes>
五、useHistory在DOM v6中被useNavigate取代
DOM v6之前:
function App() {
const history = useHistory();
const handleGoBack = () => {
history.goBack();
};
return (
<>
<button onClick={handleGoBack}>Go Back</button>
</>
);
}
DOM v6之後:
import { useNavigate } from "react-router-dom";
import "./App.css";
function App() {
const navigate = useNavigate();
const handleGoBack = () => {
navigate(-1); // new line
};
return (
<>
<button onClick={handleGoBack}>Go Back</button>
</>
);
}
目前書中還是舊版的方法,看看之後有沒有機會翻新內容,謝謝~
Hello 感謝您的回饋跟肯定
我當初在寫書的時候本來是想說範例都跟這裡一樣,所以就沒有多寫一份到Github裡面。只是後來自己修改了一部份的內容,完書的時候才注意到這點,不過自己平常時間也不夠回頭修改每個部份了,不好意思真抱歉
也有個困難點是在前端技術迭代過快,我的鐵人賽文章分別建立在React 16.9和17.2去撰寫,但在出書後半年React 18又大改了一部份內容,React router v6也是在近一年更新的。如果要所有內容都跟上最新版本實在有點吃力(畢竟我在這邊有快70篇React相關內容......)。但我會盡量回覆這裡的問題,也鼓勵讀者以互動的方式讓這裡更像是社群討論的感覺,這樣也比較能用不同人的角度去理解盲點